Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\xtask\src\worktree_teardown.rs
Line
Count
Source
1
//! Paseo worktree teardown.
2
//!
3
//! Paseo runs `worktree.teardown` commands in the host shell -
4
//! PowerShell on Windows, bash on Linux/macOS - and `paseo.json` has
5
//! no per-platform branching. Encoding the cleanup in a Rust binary
6
//! lets the same `paseo.json` entry work on every platform: `git`
7
//! invocations and env-var reads go through identical Rust APIs
8
//! regardless of the host.
9
//!
10
//! The teardown does two things:
11
//!
12
//! 1. Detach `HEAD` in the worktree so the branch is not checked out
13
//!    when paseo removes the worktree.
14
//! 2. Force-delete the worktree's branch from the source checkout so
15
//!    a future `paseo worktree create` can reuse the name without a
16
//!    manual `git branch -D`.
17
//!
18
//! Both steps are best-effort: a non-zero git exit or a failure to
19
//! spawn `git` at all is logged but does not abort teardown,
20
//! mirroring the previous shell scripts that used
21
//! `; $global:LASTEXITCODE = 0` (PowerShell) and `|| true` (bash).
22
//! Aborting would leave the worktree half-removed and force the
23
//! contributor to clean up by hand.
24
25
use std::path::{Path, PathBuf};
26
use std::process::Command;
27
28
use anyhow::{Context, Result};
29
30
/// Env var paseo sets to the worktree being torn down.
31
const ENV_WORKTREE_PATH: &str = "PASEO_WORKTREE_PATH";
32
33
/// Env var paseo sets to the source checkout (the original repo
34
/// root, shared across worktrees).
35
const ENV_SOURCE_CHECKOUT_PATH: &str = "PASEO_SOURCE_CHECKOUT_PATH";
36
37
/// Env var paseo sets to the branch name backing the worktree.
38
const ENV_BRANCH_NAME: &str = "PASEO_BRANCH_NAME";
39
40
/// All side-effecting operations performed by this subcommand.
41
///
42
/// Implement with mocks in tests to achieve zero filesystem,
43
/// environment, or process side-effects.
44
pub trait WorktreeTeardownSystem {
45
    /// Look up an environment variable.
46
    ///
47
    /// # Arguments
48
    ///
49
    /// * `key` - Environment variable name.
50
    ///
51
    /// # Returns
52
    ///
53
    /// `Some(value)` when the variable is set and non-empty,
54
    /// `None` otherwise.
55
    fn env_var(&self, key: &str) -> Option<String>;
56
57
    /// Run `git -C <repo_path> <args...>` and return the exit code.
58
    ///
59
    /// # Arguments
60
    ///
61
    /// * `repo_path` - Repository the git command targets.
62
    /// * `args` - Arguments passed after `-C <repo_path>`. Owned to
63
    ///   keep the trait `mockall`-friendly (`&[&str]` introduces
64
    ///   non-`'static` lifetimes that the mock generator rejects).
65
    ///
66
    /// # Returns
67
    ///
68
    /// The process exit code, or `-1` when the process was killed by
69
    /// a signal.
70
    ///
71
    /// # Errors
72
    ///
73
    /// Returns an error if the `git` binary cannot be spawned (for
74
    /// example, when it is not on `PATH`).
75
    fn run_git(&self, repo_path: &Path, args: Vec<String>) -> Result<i32>;
76
}
77
78
/// Production implementation of [`WorktreeTeardownSystem`].
79
pub struct RealSystem;
80
81
#[cfg_attr(coverage_nightly, coverage(off))]
82
impl WorktreeTeardownSystem for RealSystem {
83
    fn env_var(&self, key: &str) -> Option<String> {
84
        std::env::var(key).ok().filter(|v| !v.is_empty())
85
    }
86
87
    fn run_git(&self, repo_path: &Path, args: Vec<String>) -> Result<i32> {
88
        let status = Command::new("git")
89
            .arg("-C")
90
            .arg(repo_path)
91
            .args(&args)
92
            .status()
93
            .with_context(|| format!("failed to spawn `git` for {}", repo_path.display()))?;
94
        Ok(status.code().unwrap_or(-1))
95
    }
96
}
97
98
/// Tear down a paseo worktree by detaching `HEAD` and deleting the
99
/// backing branch from the source checkout.
100
///
101
/// Both git steps are best-effort: a non-zero exit code or a
102
/// spawn failure (for example, `git` missing from `PATH`) is logged
103
/// but does not abort the function. A missing env var means paseo
104
/// did not invoke us (or invoked us with an incomplete environment);
105
/// we log the gap and skip the affected step rather than failing.
106
///
107
/// # Arguments
108
///
109
/// * `system` - Injected I/O provider.
110
///
111
/// # Returns
112
///
113
/// `Ok(())` once every applicable step has been attempted.
114
5
pub fn worktree_teardown<S: WorktreeTeardownSystem>(system: &S) -> Result<()> {
115
5
    let worktree_path = system.env_var(ENV_WORKTREE_PATH);
116
5
    let source_path = system.env_var(ENV_SOURCE_CHECKOUT_PATH);
117
5
    let branch_name = system.env_var(ENV_BRANCH_NAME);
118
119
5
    if let Some(
path4
) = worktree_path.as_deref() {
120
4
        run_git_best_effort(
121
4
            system,
122
4
            &PathBuf::from(path),
123
4
            vec!["checkout".to_owned(), "--detach".to_owned()],
124
4
            &format!("`git checkout --detach` in {path}"),
125
4
        );
126
4
    } else {
127
1
        log::info!("paseo worktree teardown: {ENV_WORKTREE_PATH} not set; skipping HEAD detach.");
128
    }
129
130
5
    match (source_path.as_deref(), branch_name.as_deref()) {
131
4
        (Some(source), Some(branch)) => {
132
4
            run_git_best_effort(
133
4
                system,
134
4
                &PathBuf::from(source),
135
4
                vec!["branch".to_owned(), "-D".to_owned(), branch.to_owned()],
136
4
                &format!("`git branch -D {branch}` in {source}"),
137
4
            );
138
4
        }
139
        _ => {
140
1
            log::info!(
141
                "paseo worktree teardown: {ENV_SOURCE_CHECKOUT_PATH} or {ENV_BRANCH_NAME} not set; skipping branch delete."
142
            );
143
        }
144
    }
145
146
5
    Ok(())
147
5
}
148
149
8
fn run_git_best_effort<S: WorktreeTeardownSystem>(
150
8
    system: &S,
151
8
    repo_path: &Path,
152
8
    args: Vec<String>,
153
8
    description: &str,
154
8
) {
155
8
    match system.run_git(repo_path, args) {
156
4
        Ok(0) => {}
157
2
        Ok(code) => {
158
2
            log::warn!(
159
                "paseo worktree teardown: {description} exited with code {code}; continuing."
160
            );
161
        }
162
2
        Err(err) => {
163
2
            log::warn!(
164
                "paseo worktree teardown: {description} failed to spawn `git`: {err:#}; continuing."
165
            );
166
        }
167
    }
168
8
}
169
170
#[cfg(test)]
171
#[path = "tests/test_worktree_teardown.rs"]
172
mod tests;